Skip to content

Avoid recursive blanket impl checks#155765

Open
chenyukang wants to merge 2 commits intorust-lang:mainfrom
chenyukang:yukang-fix-155759-rustdoc-blanket-impl
Open

Avoid recursive blanket impl checks#155765
chenyukang wants to merge 2 commits intorust-lang:mainfrom
chenyukang:yukang-fix-155759-rustdoc-blanket-impl

Conversation

@chenyukang
Copy link
Copy Markdown
Member

@chenyukang chenyukang commented Apr 25, 2026

View all comments

Fixes #155759

Avoid recursively re-checking rustdoc blanket impl candidates while synthesizing impls.

Also record the polarity of synthesized auto-trait impls and reuses that result when checking blanket impl predicates.

If we want to assert the test of rustdoc must finished in specific time, we need to add a run-make test instead?

I tests it now runs about 2.2s for 60 variants on my local machine.

@rustbot rustbot added the S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. label Apr 25, 2026
@rustbot
Copy link
Copy Markdown
Collaborator

rustbot commented Apr 25, 2026

r? @fmease

rustbot has assigned @fmease.
They will have a look at your PR within the next two weeks and either review your PR or reassign to another reviewer.

Use r? to explicitly pick a reviewer

Why was this reviewer chosen?

The reviewer was selected based on:

  • Owners of files modified in this PR: rustdoc
  • rustdoc expanded to 9 candidates
  • Random selection from GuillaumeGomez, fmease, lolbinarycat, notriddle

@rustbot rustbot added T-rustdoc Relevant to the rustdoc team, which will review and decide on the PR/issue. T-rustdoc-frontend Relevant to the rustdoc-frontend team, which will review and decide on the web UI/UX output. labels Apr 25, 2026
@rust-log-analyzer

This comment has been minimized.

@rustbot rustbot added A-rustdoc-js Area: Rustdoc's JS front-end A-rustdoc-search Area: Rustdoc's search feature labels Apr 25, 2026
@chenyukang
Copy link
Copy Markdown
Member Author

chenyukang commented Apr 25, 2026

The job aarch64-gnu-llvm-21-1 failed! Check out the build log: (web) (plain enhanced) (plain)

Click to see the possible cause of the failure (guessed by this bot)

Executing "/scripts/stage_2_test_set1.sh"
+ /scripts/stage_2_test_set1.sh
PR_CI_JOB set; skipping tidy
+ '[' 1 == 1 ']'
+ echo 'PR_CI_JOB set; skipping tidy'
+ SKIP_TIDY='--skip tidy'
+ ../x.py --stage 2 test --skip tidy --skip compiler --skip src
##[group]Building bootstrap
    Finished `dev` profile [unoptimized] target(s) in 0.04s
##[endgroup]
downloading https://static.rust-lang.org/dist/2026-04-14/rustfmt-nightly-aarch64-unknown-linux-gnu.tar.xz
---

---- [rustdoc-js] tests/rustdoc-js/never-search.rs stdout ----
------node stdout------------------------------
Testing /checkout/tests/rustdoc-js/never-search.js ... FAILED
[ query `! ->`]==> Expected exactly 2 results but found 3 in 'others'
[ query `! ->`]==> Exact check failed at position 0: expected '{"path":"never_search","name":"impossible"}' but found '{"crate":"never_search","name":"from","path":"never_search","exactPath":"never_search","ty":13,"parent":{"path":"never_search","exactPath":"never_search","name":"never","ty":5},"type":{"inputs":[{"id":11,"name":"never","ty":1,"path":"","exactPath":"","generics":[],"bindings":{},"unboxFlag":false}],"output":[{"id":-1,"name":"","ty":26,"path":null,"exactPath":null,"generics":[],"bindings":{},"unboxFlag":true}],"where_clause":[[]]},"paramNames":["T"],"dist":2,"path_dist":0,"index":-1,"desc":"","item":{"id":7,"crate":"never_search","ty":13,"name":"from","normalizedName":"from","modulePath":"never_search","exactModulePath":"never_search","entry":{"krate":27,"ty":13,"modulePath":27,"exactModulePath":27,"parent":12,"traitParent":4,"deprecated":false,"unstable":false,"associatedItemDisambiguatorOrExternCrateUrl":null},"path":null,"functionData":{"functionSignature":{"inputs":[{"id":11,"name":"never","ty":1,"path":"","exactPath":"","generics":[],"bindings":{},"unboxFlag":false}],"output":[{"id":-1,"name":"","ty":26,"path":null,"exactPath":null,"generics":[],"bindings":{},"unboxFlag":true}],"where_clause":[[]]},"paramNames":["T"],"elemCount":2},"deprecated":false,"unstable":false,"parent":{"name":"never","path":{"ty":5,"modulePath":"never_search","exactModulePath":"never_search"}},"traitParent":{"name":"From","path":{"ty":10,"modulePath":"core::convert","exactModulePath":"core::convert"}}},"displayPath":"<span>never_search::</span><span>never::</span>","fullPath":"never_search::never::from|13","traitPath":"core::convert::From::from|13","href":"../never_search/struct.never.html#method.from","displayTypeSignature":{"type":["","!"," -> T"],"mappedNames":{},"whereClause":{}},"id":7,"elems":[{"name":"never","id":11,"typeFilter":1,"generics":[],"bindings":{},"fullPath":["never"],"pathLast":"never","normalizedPathLast":"never","pathWithoutLast":[]}],"returned":[],"is_alias":false,"displayType":"`!` -> T","displayMappedNames":"","displayWhereClause":""}'
[ query `! ->`]==> Exact check failed at position 0: expected '{"path":"never_search","name":"box_impossible"}' but found '{"crate":"never_search","name":"impossible","path":"never_search","exactPath":"never_search","ty":7,"type":{"inputs":[{"id":11,"name":"never","ty":1,"path":"","exactPath":"","generics":[],"bindings":{},"unboxFlag":false}],"output":[{"id":9,"name":"unit","ty":1,"path":"","exactPath":"","generics":[],"bindings":{},"unboxFlag":false}],"where_clause":[]},"paramNames":[],"dist":2,"path_dist":0,"index":-1,"desc":"","item":{"id":26,"crate":"never_search","ty":7,"name":"impossible","normalizedName":"impossible","modulePath":"never_search","exactModulePath":"never_search","entry":{"krate":27,"ty":7,"modulePath":27,"exactModulePath":null,"parent":null,"traitParent":null,"deprecated":false,"unstable":false,"associatedItemDisambiguatorOrExternCrateUrl":null},"path":null,"functionData":{"functionSignature":{"inputs":[{"id":11,"name":"never","ty":1,"path":"","exactPath":"","generics":[],"bindings":{},"unboxFlag":false}],"output":[{"id":9,"name":"unit","ty":1,"path":"","exactPath":"","generics":[],"bindings":{},"unboxFlag":false}],"where_clause":[]},"paramNames":[],"elemCount":2},"deprecated":false,"unstable":false,"parent":null,"traitParent":null},"displayPath":"<span>never_search::</span>","fullPath":"never_search::impossible|7","traitPath":null,"href":"../never_search/fn.impossible.html","displayTypeSignature":{"type":["","!"," -> ()"],"mappedNames":{},"whereClause":{}},"id":26,"elems":[{"name":"never","id":11,"typeFilter":1,"generics":[],"bindings":{},"fullPath":["never"],"pathLast":"never","normalizedPathLast":"never","pathWithoutLast":[]}],"returned":[],"is_alias":false,"displayType":"`!` -> ()","displayMappedNames":"","displayWhereClause":""}'

------node stderr------------------------------

------------------------------------------

error: rustdoc-js test failed!
status: exit status: 1
command: "/usr/bin/node" "/checkout/src/tools/rustdoc-js/tester.js" "--doc-folder" "/checkout/obj/build/aarch64-unknown-linux-gnu/test/rustdoc-js/never-search" "--crate-name" "never_search" "--test-file" "/checkout/tests/rustdoc-js/never-search.js" "--revision" ""
--- stdout -------------------------------
Testing /checkout/tests/rustdoc-js/never-search.js ... FAILED
[ query `! ->`]==> Expected exactly 2 results but found 3 in 'others'
[ query `! ->`]==> Exact check failed at position 0: expected '{"path":"never_search","name":"impossible"}' but found '{"crate":"never_search","name":"from","path":"never_search","exactPath":"never_search","ty":13,"parent":{"path":"never_search","exactPath":"never_search","name":"never","ty":5},"type":{"inputs":[{"id":11,"name":"never","ty":1,"path":"","exactPath":"","generics":[],"bindings":{},"unboxFlag":false}],"output":[{"id":-1,"name":"","ty":26,"path":null,"exactPath":null,"generics":[],"bindings":{},"unboxFlag":true}],"where_clause":[[]]},"paramNames":["T"],"dist":2,"path_dist":0,"index":-1,"desc":"","item":{"id":7,"crate":"never_search","ty":13,"name":"from","normalizedName":"from","modulePath":"never_search","exactModulePath":"never_search","entry":{"krate":27,"ty":13,"modulePath":27,"exactModulePath":27,"parent":12,"traitParent":4,"deprecated":false,"unstable":false,"associatedItemDisambiguatorOrExternCrateUrl":null},"path":null,"functionData":{"functionSignature":{"inputs":[{"id":11,"name":"never","ty":1,"path":"","exactPath":"","generics":[],"bindings":{},"unboxFlag":false}],"output":[{"id":-1,"name":"","ty":26,"path":null,"exactPath":null,"generics":[],"bindings":{},"unboxFlag":true}],"where_clause":[[]]},"paramNames":["T"],"elemCount":2},"deprecated":false,"unstable":false,"parent":{"name":"never","path":{"ty":5,"modulePath":"never_search","exactModulePath":"never_search"}},"traitParent":{"name":"From","path":{"ty":10,"modulePath":"core::convert","exactModulePath":"core::convert"}}},"displayPath":"<span>never_search::</span><span>never::</span>","fullPath":"never_search::never::from|13","traitPath":"core::convert::From::from|13","href":"../never_search/struct.never.html#method.from","displayTypeSignature":{"type":["","!"," -> T"],"mappedNames":{},"whereClause":{}},"id":7,"elems":[{"name":"never","id":11,"typeFilter":1,"generics":[],"bindings":{},"fullPath":["never"],"pathLast":"never","normalizedPathLast":"never","pathWithoutLast":[]}],"returned":[],"is_alias":false,"displayType":"`!` -> T","displayMappedNames":"","displayWhereClause":""}'
[ query `! ->`]==> Exact check failed at position 0: expected '{"path":"never_search","name":"box_impossible"}' but found '{"crate":"never_search","name":"impossible","path":"never_search","exactPath":"never_search","ty":7,"type":{"inputs":[{"id":11,"name":"never","ty":1,"path":"","exactPath":"","generics":[],"bindings":{},"unboxFlag":false}],"output":[{"id":9,"name":"unit","ty":1,"path":"","exactPath":"","generics":[],"bindings":{},"unboxFlag":false}],"where_clause":[]},"paramNames":[],"dist":2,"path_dist":0,"index":-1,"desc":"","item":{"id":26,"crate":"never_search","ty":7,"name":"impossible","normalizedName":"impossible","modulePath":"never_search","exactModulePath":"never_search","entry":{"krate":27,"ty":7,"modulePath":27,"exactModulePath":null,"parent":null,"traitParent":null,"deprecated":false,"unstable":false,"associatedItemDisambiguatorOrExternCrateUrl":null},"path":null,"functionData":{"functionSignature":{"inputs":[{"id":11,"name":"never","ty":1,"path":"","exactPath":"","generics":[],"bindings":{},"unboxFlag":false}],"output":[{"id":9,"name":"unit","ty":1,"path":"","exactPath":"","generics":[],"bindings":{},"unboxFlag":false}],"where_clause":[]},"paramNames":[],"elemCount":2},"deprecated":false,"unstable":false,"parent":null,"traitParent":null},"displayPath":"<span>never_search::</span>","fullPath":"never_search::impossible|7","traitPath":null,"href":"../never_search/fn.impossible.html","displayTypeSignature":{"type":["","!"," -> ()"],"mappedNames":{},"whereClause":{}},"id":26,"elems":[{"name":"never","id":11,"typeFilter":1,"generics":[],"bindings":{},"fullPath":["never"],"pathLast":"never","normalizedPathLast":"never","pathWithoutLast":[]}],"returned":[],"is_alias":false,"displayType":"`!` -> ()","displayMappedNames":"","displayWhereClause":""}'
------------------------------------------
stderr: none

---- [rustdoc-js] tests/rustdoc-js/never-search.rs stdout end ----

The CI failure is caused after this PR,
rustdoc now includes the blanket impl method from:

impl<T> From<!> for T

when searching ! ->.

I think it's ok to update the test case.

Copy link
Copy Markdown
Member

@fmease fmease Apr 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can just be a rustdoc UI test since we don't care about the HTML. For that move, it into tests/rustdoc-ui/, remove the #![crate_name], the htmldocck directives and add //@ check-pass

View changes since the review

@@ -0,0 +1,268 @@
#![crate_name = "foo"]
Copy link
Copy Markdown
Member

@fmease fmease Apr 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In a source code comment at the top, can you add which initial (auto trait, ADT) pairing(s) used to cause the blowout (unless that info isn't meaningful)

View changes since the review

Copy link
Copy Markdown
Member

@fmease fmease Apr 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm curious, does this have any effect on issue #114891?

View changes since the review

Comment on lines +72 to +74
let predicate = infcx.resolve_vars_if_possible(predicate);
if let Some(may_apply) =
cached_auto_trait_predicate_result(cx, predicate, impl_ty, item_ty)
Copy link
Copy Markdown
Member

@fmease fmease Apr 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's very odd to me that we have to special case auto traits here when synthesizing blanket impls, it feels like we're manually helping out the trait solver here when it's likely the old trait solver's fault?

Well, I don't actually know the actually fast-expanding tree of obligations in dtolnay's example. Could you enlighten me?

View changes since the review

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm gonna rebase my dusty PR #125907, impl lcnr's final suggestion and check if that PR also fixes the issue.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#125907 does indeed fix the issue, too.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, so next solver should be a better fix for it.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's very odd to me that we have to special case auto traits here when synthesizing blanket impls, it feels like we're manually helping out the trait solver here when it's likely the old trait solver's fault?

Well, I don't actually know the actually fast-expanding tree of obligations in dtolnay's example. Could you enlighten me?

View changes since the review

It's very odd to me that we have to special case auto traits here when synthesizing blanket impls, it feels like we're manually helping out the trait solver here when it's likely the old trait solver's fault?

Well, I don't actually know the actually fast-expanding tree of obligations in dtolnay's example. Could you enlighten me?

View changes since the review

The expanding obligation tree I had in mind is from checking the blanket impl:

impl<T: Send + Sync> ThreadSafe for T {}

for Class<Span>.

To synthesize impl ThreadSafe for Class<Span>, rustdoc needs to prove the impl predicates:

Class<Span>: Send
Class<Span>: Sync

Each of those auto-trait goals expands through the fields of Class:

Class<Span>
  -> Arc<[Statement<Span>]>
  -> Arc<[Expression<Span>]>

Here N means the number of recursive wrapper variants in each enum: the
S00..S(N-1) variants in Statement and the E00..E(N-1) variants in
Expression. In dtolnay's example, N = 60 for each of those two enums.

Then Statement and Expression each contain two kinds of recursive edges:

  1. a Class(Arc<Class<Span>>) variant, which leads back to Class<Span>;
  2. N wrapper variants, each of which leads back to the same enum type.

So one layer of expansion is already O(N):

Statement<Span>: Send
  -> Class variant -> Class<Span>: Send/Sync
  -> S00<Span>: Send -> Statement<Span>: Send
  -> S01<Span>: Send -> Statement<Span>: Send
  ...
  -> S(N-1)<Span>: Send -> Statement<Span>: Send

and similarly for Expression<Span>.

The shape is a recursive cycle with an O(N) fan-out at each layer:

Class
  -> Statement / Expression
     -> Class
     -> N wrapper variants
        -> Statement / Expression
           -> Class
           -> N wrapper variants
              -> ...

If the same auto-trait goals are re-expanded instead of being reused as part of the recursive cycle, the explored tree grows roughly like:

1 + O(N) + O(N^2) + O(N^3) + ...

In dtolnay's example, that matches the observed behavior: adding 10 more variants roughly doubled the rustdoc runtime. So the practical behavior is exponential-like in the number of variants.

Comment thread src/librustdoc/core.rs
Comment on lines 61 to +63
pub(crate) generated_synthetics: FxHashSet<(Ty<'tcx>, DefId)>,
/// Polarity of synthesized auto-trait impls processed so far.
pub(crate) generated_auto_trait_impls: FxHashMap<(Ty<'tcx>, DefId), ty::ImplPolarity>,
Copy link
Copy Markdown
Member

@fmease fmease Apr 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't try to address this now since I want to test my other PR first. I'd just like to note that it's confusing to name this generated_auto_trait_impls because generated_synthetics directly above caches both synthetic blanket and synthetic auto trait impls (which is actually problematic, they should be separate caches, see #148980) meaning we would now have "two" auto trait impl caches which are slightly different.

View changes since the review

@@ -4,6 +4,7 @@ const EXPECTED = [
{
'query': '! ->',
Copy link
Copy Markdown
Member

@fmease fmease Apr 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I need to ponder about this behavior change; obviously optimizations like caching shouldn't observably affect the semantics of a program and if they do that indicates a bug somewhere even if the change is 'desirable'.

I've just woken up and I've only skimmed this PR, so this is just a hunch: I see you're somehow involving ImplPolarity in your cache and the impl in question is impl<T> From<!> for T which is a reservation impl (ImplPolarity::Reservation), so you might not be handling that 'correctly'.

Again, it's possible that the change is desirable (I haven't thought about that yet) but then that should be explicitly encoded outside of the caching logic.

View changes since the review

E58(std::sync::Arc<E58<Span>>),
E59(std::sync::Arc<E59<Span>>),
}
}
Copy link
Copy Markdown
Member

@fmease fmease Apr 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we want to assert the test of rustdoc must finished in specific time, we need to add a run-make test instead?

That seems iffy to me as it can easily render the test flaky. It could lead to the test failing on slow machines or in CI if the OS is under heavy workload and suspends the process for a while.

View changes since the review

@fmease fmease added S-blocked Status: Blocked on something else such as an RFC or other implementation work. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels May 7, 2026
@fmease
Copy link
Copy Markdown
Member

fmease commented May 8, 2026

I didn't forget about this, I'll revisit this soon. Having such a targeted fix seems a bit iffy but OTOH the next-solver migration might be blocked on next-solver perf improvements (need to double check if there's anything else I can do instead of that).

I'm curious @bors try @rust-timer queue

@rust-timer

This comment has been minimized.

@rustbot rustbot added the S-waiting-on-perf Status: Waiting on a perf run to be completed. label May 8, 2026
@rust-bors

This comment has been minimized.

rust-bors Bot pushed a commit that referenced this pull request May 8, 2026
…impl, r=<try>

Avoid recursive blanket impl checks
@rust-bors
Copy link
Copy Markdown
Contributor

rust-bors Bot commented May 8, 2026

☀️ Try build successful (CI)
Build commit: 1260cee (1260cee76b9db01961d2614373bb4f6cb1d3c05f, parent: f964de49bcb561e5c6c725bb37201e11d852daf0)

@rust-timer

This comment has been minimized.

@rust-timer
Copy link
Copy Markdown
Collaborator

Finished benchmarking commit (1260cee): comparison URL.

Overall result: ❌✅ regressions and improvements - please read:

Benchmarking means the PR may be perf-sensitive. It's automatically marked not fit for rolling up. Overriding is possible but disadvised: it risks changing compiler perf.

Next, please: If you can, justify the regressions found in this try perf run in writing along with @rustbot label: +perf-regression-triaged. If not, fix the regressions and do another perf run. Neutral or positive results will clear the label automatically.

@bors rollup=never
@rustbot label: -S-waiting-on-perf +perf-regression

Instruction count

Our most reliable metric. Used to determine the overall result above. However, even this metric can be noisy.

mean range count
Regressions ❌
(primary)
- - 0
Regressions ❌
(secondary)
0.4% [0.4%, 0.4%] 1
Improvements ✅
(primary)
-0.7% [-2.4%, -0.2%] 13
Improvements ✅
(secondary)
-1.4% [-2.1%, -0.4%] 3
All ❌✅ (primary) -0.7% [-2.4%, -0.2%] 13

Max RSS (memory usage)

Results (primary -2.8%, secondary -0.5%)

A less reliable metric. May be of interest, but not used to determine the overall result above.

mean range count
Regressions ❌
(primary)
- - 0
Regressions ❌
(secondary)
2.5% [2.5%, 2.5%] 1
Improvements ✅
(primary)
-2.8% [-4.6%, -1.5%] 5
Improvements ✅
(secondary)
-1.6% [-2.4%, -1.1%] 3
All ❌✅ (primary) -2.8% [-4.6%, -1.5%] 5

Cycles

Results (primary -2.5%, secondary 0.2%)

A less reliable metric. May be of interest, but not used to determine the overall result above.

mean range count
Regressions ❌
(primary)
- - 0
Regressions ❌
(secondary)
0.9% [0.4%, 1.2%] 5
Improvements ✅
(primary)
-2.5% [-3.2%, -2.0%] 3
Improvements ✅
(secondary)
-1.6% [-2.3%, -0.9%] 2
All ❌✅ (primary) -2.5% [-3.2%, -2.0%] 3

Binary size

This perf run didn't have relevant results for this metric.

Bootstrap: 508.875s -> 500.388s (-1.67%)
Artifact size: 395.04 MiB -> 395.05 MiB (0.00%)

@rustbot rustbot added perf-regression Performance regression. and removed S-waiting-on-perf Status: Waiting on a perf run to be completed. labels May 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-rustdoc-js Area: Rustdoc's JS front-end A-rustdoc-search Area: Rustdoc's search feature perf-regression Performance regression. S-blocked Status: Blocked on something else such as an RFC or other implementation work. T-rustdoc Relevant to the rustdoc team, which will review and decide on the PR/issue. T-rustdoc-frontend Relevant to the rustdoc-frontend team, which will review and decide on the web UI/UX output.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Exponentially slow rustdoc trait impl computation

5 participants